<?php

/**
 * This file is part of the Framework03 PHP framework.
 * @author Walter Tamboer
 * @package Framework03
 */

/**
 * A HTTP client.
 */
class FW_Http_Client
{
    /**
     * @var FW_Http_Adapter_Abstract The adapter used to communicate.
     */
    private $adapter = null;

    /**
     * @var FW_Uri_Abstract The URI to connect to.
     */
    private $uri = '';

    /**
     * @var string The method of the current request.
     */
    private $method = FW_Http_Request::GET;

    /**
     * @var array The options used to do requests.
     */
    private $options = array(
        'adapter'         => 'FW_Http_Adapter_Socket',
        'httpversion'     => FW_Http_Request::HTTP_1_1,
        'keepalive'       => false,
        'maxredirects'    => 5,
        'timeout'         => 10,
        'storeresponse'   => true,
        'strict'          => true,
        'strictredirects' => false,
        'useragent'       => 'FW_Http_Client'
    );

    /**
     * @var array The parameters that are sent with the request.
     */
    private $parameters = array();

    /**
     * @var string The encoding type of this HTTP request.
     */
    private $encodingType;

    /**
     * @var array Additional headers to send in the request.
     */
    private $headers = array();

    /**
     * Initializes a new instance of this class.
     * @param string|FW_Uri_Abstract $uri The URI to do a request on.
     * @param array $options The options of this client.
     */
    public function __construct($uri = '', array $options = array())
    {
        $this->setUri($uri);
        $this->setOptions($options);
    }

    /**
     * Sets the options of this HTTP client.
     * @param array $options The options to set.
     * @return FW_Http_Client
     */
    public function setOptions(array $options)
    {
        $this->options = array_merge($this->options, $options);
        return $this;
    }
    
    /**
     * Gets the options of this HTTP client.
     * @return array
     */
    public function getOptions()
    {
        return $this->options;
    }

    /**
     * Sets the URI to do a request on.
     * @param string|FW_Uri_Abstract $uri The URI to do a request on.
     * @return FW_Http_Client
     */
    public function setUri($uri)
    {
        $this->uri = FW_Uri::factory($uri);
        return $this;
    }

    /**
     * Gets the URI of this HTTP client.
     * @return FW_Uri_Abstract
     */
    public function getUri()
    {
        return $this->uri;
    }

    /**
     * Sets the method of the request.
     * @param string $method
     * @return FW_Http_Client
     */
    public function setMethod($method)
    {
        $this->method = $method;
        return $this;
    }

    /**
     * Gets the method of this HTTP request.
     * @return string
     */
    public function getMethod()
    {
        return $this->method;
    }

    /**
     * Sets a parameter of this request.
     * @param string $name The name of the parameter.
     * @param string $value The value of the parameter.
     * @return FW_Http_Client
     */
    public function setParameter($name, $value)
    {
        $this->parameters[$name] = $value;
        return $this;
    }

    /**
     * Sets a header for this request.
     * @param string $name The name of the header.
     * @param string $value The value of the header.
     * @return FW_Http_Client
     */
    public function setHeader($name, $value)
    {
        $this->headers[strtolower($name)] = $value;
        return $this;
    }

    /**
     * Sets the encoding type of this request.
     * @param string $type
     * @return FW_Http_Client
     */
    public function setEncodingType($type)
    {
        $this->encodingType = $type;
        return $this;
    }

    /**
     * Does the actual request.
     * @param string $method The method used to do a request.
     * @return FW_Http_Response
     */
    public function request($method = null)
    {
        if (!($this->uri instanceof FW_Uri_Http) && !($this->uri instanceof FW_Uri_Https))
            throw new FW_Http_Client_Exception('No valid uri has been passed to the client.');

        if ($this->adapter == null)
            $this->adapter = new $this->options['adapter'];
            
        if ($method != null)
            $this->method = $method;

        $this->adapter->setOption('timeout', $this->options['timeout']);

        $redirectCounter = 0;

        do
        {
            $uri = clone $this->uri;

            if ($this->method == FW_Http_Request::GET && !empty($this->parameters))
            {
                $query = $uri->getQuery();

                if (!empty($query))
                    $query .= '&';

                $query .= http_build_query($this->parameters, null, '&');
                $uri->setQuery($query);
            }

            // Create the headers and body which are the actual request.
            $body = $this->createBody();
            $headers = $this->createHeaders();

            // Connect to the host...
            $this->adapter->connect($uri->getHost(), $uri->getPort(), ($uri->getScheme() == 'https' ? true : false));

            // ... and send the request.
            $this->request = $this->adapter->write($this->method, $uri, $this->options['httpversion'], $headers, $body);

            $response = $this->adapter->read();

            if (!$response)
                throw new FW_Http_Client_Exception('Unable to read response, or response is empty');

            $response = FW_Http_Response::fromString($response);
            
            if ($response->isRedirect() && ($location = $response->getHeader('location')))
            {
                ++$redirectCounter;
                throw new FW_Exception('TODO: Handle redirects', 500);
            }
            else
                break; // No redirecting, so break out of the loop.
        }
        while ($redirectCounter < $this->options['maxredirects']);

        return $response;
    }

    public function createHeaders()
    {
        $headers = $this->headers;

        if (!isset($headers['host']))
        {
            $scheme = $this->uri->getScheme();
            $host = $this->uri->getHost();
            $port = $this->uri->getPort();

            if (($scheme == 'http' && $port != 80) || ($scheme == 'https' && $port == 443))
                $host .= ':' . $port;

            $headers['host'] = $host;
        }

        return $headers;
    }

    public function createBody()
    {
        // According to RFC2616, a TRACE request should not have a body.
        if ($this->method == FW_Http_Request::TRACE)
            return '';

        $body = '';

        if ($this->method == FW_Http_Request::POST && count($this->parameters) > 0)
        {
            if ($this->encodingType == '')
                $this->encodingType = FW_Http_Request::ENC_URLENCODED;

            $this->setHeader('Content-Type', $this->encodingType);

            switch ($this->encodingType)
            {
            case FW_Http_Request::ENC_URLENCODED:
                {
                    foreach ($this->parameters as $name => $value)
                        $body .= $name . '=' . $value . "&";
                }
                break;

            case FW_Http_Request::ENC_FORMDATA:
                {
                }
                break;
            }
        }

        if ($body)
            $this->setHeader('Content-Length', strlen($body));

        return $body;
    }
}
